<?php
// +----------------------------------------------------------------------
// | OneThink [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2013 http://www.onethink.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://www.zjzit.cn>
// +----------------------------------------------------------------------

namespace Admin\Controller;

use Think\Db;
use OT\Database;

/**
 * 数据库备份还原控制器
 * 改动 @icy8
 * @author 麦当苗儿 <zuojiazi@vip.qq.com>
 */
class DatabaseController extends AdminController
{

    /**
     * 数据库备份/还原列表
     * @param  String $type import-还原，export-备份
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function index($type = null)
    {
        $listData = array(
            'url' => U('', array('type' => $type)),
            'toolbar' => '#toolbar',
            'page' => false,
        );
        switch ($type) {
            /* 数据还原 */
            case 'import':
                $this->IS_POST(function () {
                    //列出备份文件列表
                    $path = realpath(C('DATA_BACKUP_PATH'));
                    $flag = \FilesystemIterator::KEY_AS_FILENAME;
                    $glob = new \FilesystemIterator($path, $flag);

                    $list = array();
                    foreach ($glob as $name => $file) {
                        if (preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)) {
                            $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');
                            $date = "{$name[0]}-{$name[1]}-{$name[2]}";
                            $time = "{$name[3]}:{$name[4]}:{$name[5]}";
                            $time_stmp = strtotime("{$date} {$time}");
                            $part = $name[6];
                            if (isset($list[$time_stmp])) {
                                $info = $list["{$date} {$time}"];
                                $info['part'] = max($info['part'], $part);
                                $info['size'] = $info['size'] + $file->getSize();
                            } else {
                                $info['part'] = $part;
                                $info['size'] = $file->getSize();
                            }
                            $info['size'] = format_bytes($info['size']);
                            $extension = strtoupper(pathinfo($file->getFilename(), PATHINFO_EXTENSION));
                            $info['compress'] = ($extension === 'SQL') ? '-' : $extension;
                            $info['name'] = date('Ymd-His', $time_stmp);
                            $info['time'] = "{$date} {$time}";
                            $list[] = $info;
                        }
                    }
                    $data = array(
                        'data' => $list,
                        'code' => 0,
                    );
                    $this->ajaxReturn($data);
                });
                $this->_web('title', '数据库还原');
                $this->_web('panel_title', '数据库还原');
                $listData['toolbarTpl'] = 'import_tool'; // 工具条模版
                $listData['cols'][] = array('type' => 'checkbox', 'fixed' => 'left', 'LAY_CHECKED' => true);
                $listData['cols'][] = array('field' => 'name', 'title' => '备份名称', 'width' => '15%');
                $listData['cols'][] = array('field' => 'part', 'title' => '卷数', 'width' => '8%');
                $listData['cols'][] = array('field' => 'compress', 'title' => '压缩', 'width' => '8%');
                $listData['cols'][] = array('field' => 'size', 'title' => '数据大小', 'width' => '15%');
                $listData['cols'][] = array('field' => 'time', 'title' => '备份时间', 'width' => '15%');
                $listData['cols'][] = array('fixed' => 'right', 'title' => '操作', 'toolbar' => '#rowBar');
                break;
            /* 数据备份 */
            case 'export':
                $this->IS_POST(function () {
                    $Db = Db::getInstance();
                    $list = $Db->query('SHOW TABLE STATUS');
                    foreach ($list as &$row) {
                        $row = array_change_key_case($row, CASE_LOWER);
                        $row['status'] = '未备份';
                    }
                    $data = array(
                        'code' => 0,
                        'msg' => '',
                        'data' => $list,
                    );
                    $this->ajaxReturn($data);
                });
                $this->_web('title', '数据备份');
                $this->_web('panel_title', '数据备份');
                $listData['toolbarTpl'] = 'export_tool'; // 工具条模版
                $listData['cols'][] = array('type' => 'checkbox', 'fixed' => 'left', 'LAY_CHECKED' => true);
                $listData['cols'][] = array('field' => 'name', 'title' => '表名', 'width' => '15%');
                $listData['cols'][] = array('field' => 'rows', 'title' => '数据量', 'width' => '8%');
                $listData['cols'][] = array('field' => 'data_length', 'title' => '数据大小', 'width' => '9%');
                $listData['cols'][] = array('field' => 'create_time', 'title' => '创建时间', 'width' => '15%');
                $listData['cols'][] = array('field' => 'status', 'title' => '备份状态', 'width' => '15%');
                $listData['cols'][] = array('fixed' => 'right'/*, width => '200'*/, 'title' => '操作', 'toolbar' => '#rowBar');
                break;
            default:
                $this->error('参数错误！');
        }
        $this->displayList($listData);
    }

    /**
     * 优化表
     * @param  String $tables 表名
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function optimize($tables = null)
    {
        if ($tables) {
            $Db = Db::getInstance();
            if (is_array($tables)) {
                $tables = implode('`,`', $tables);
                $list = $Db->query("OPTIMIZE TABLE `{$tables}`");

                if ($list) {
                    $this->success("数据表优化完成！");
                } else {
                    $this->error("数据表优化出错请重试！");
                }
            } else {
                $list = $Db->query("OPTIMIZE TABLE `{$tables}`");
                if ($list) {
                    $this->success("数据表'{$tables}'优化完成！");
                } else {
                    $this->error("数据表'{$tables}'优化出错请重试！");
                }
            }
        } else {
            $this->error("请指定要优化的表！");
        }
    }

    /**
     * 修复表
     * @param  String $tables 表名
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function repair($tables = null)
    {
        if ($tables) {
            $Db = Db::getInstance();
            if (is_array($tables)) {
                $tables = implode('`,`', $tables);
                $list = $Db->query("REPAIR TABLE `{$tables}`");

                if ($list) {
                    $this->success("数据表修复完成！");
                } else {
                    $this->error("数据表修复出错请重试！");
                }
            } else {
                $list = $Db->query("REPAIR TABLE `{$tables}`");
                if ($list) {
                    $this->success("数据表'{$tables}'修复完成！");
                } else {
                    $this->error("数据表'{$tables}'修复出错请重试！");
                }
            }
        } else {
            $this->error("请指定要修复的表！");
        }
    }

    /**
     * 删除备份文件
     * 增加批量删除 2019 01 14 icy8
     * @param  Integer $name 备份名称
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function del($name = 0)
    {
        if ($name) {
            $files = array();
            if(is_array($name)) {
                foreach ($name as $r) {
                    $file = $r . '-*.sql*'; // 关联卷
                    $path = realpath(C('DATA_BACKUP_PATH')) . DIRECTORY_SEPARATOR . $file;
                    $files = array_merge($files, glob($path));
                }
            }else{
                $name = $name . '-*.sql*'; // 关联卷
                $path = realpath(C('DATA_BACKUP_PATH')) . DIRECTORY_SEPARATOR . $name;
                $files = glob($path);
            }
            array_map("unlink", $files);
            if (count(glob($path))) {
                $this->success('备份文件删除失败，请检查权限！');
            } else {
                $this->success('备份文件删除成功！');
            }
        } else {
            $this->error('参数错误！');
        }
    }

    /**
     * 备份数据库
     * 修改：取消逐表响应方式，改用“堵塞式”备份。 2019 01 14 icy8
     * @param  String $tables 表名
     * @param  Integer $id 表ID
     * @param  Integer $start 起始行数
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function export($tables = null)
    {
        set_time_limit(0); // 超时设置
        //读取备份配置
        $config = array(
            'path' => realpath(C('DATA_BACKUP_PATH')) . DIRECTORY_SEPARATOR,
            'part' => C('DATA_BACKUP_PART_SIZE'),
            'compress' => C('DATA_BACKUP_COMPRESS'),
            'level' => C('DATA_BACKUP_COMPRESS_LEVEL'),
        );
        //生成备份文件信息
        $file = array(
            'name' => date('Ymd-His', NOW_TIME),
            'part' => 1,
        );
        //检查是否有正在执行的任务
        $lock = "{$config['path']}backup.lock";
        if (is_file($lock)) {
            $this->error('检测到有一个备份任务正在执行，请稍后再试！');
        } else {
            //创建锁文件
            file_put_contents($lock, NOW_TIME);
        }
        //检查备份目录是否可写
        is_writeable($config['path']) || $this->error('备份目录不存在或不可写，请检查后重试！');
        //创建备份文件
        $Database = new Database($file, $config);
        // 初始化备份文件
        if (false === $Database->create()) {
            $this->error('初始化失败，备份文件创建失败！');
        }
        $i = $start = 0;
        $status = 1;
        while (1) {
            $table = $tables[$i];
            if (!$table) break; // 备份完毕
            $start = $Database->backup($table, $start);
            if (false === $start) { //出错
                $status = 0;
                break;
            } else if ($start === 0) {
                // 继续下一表
                $i++;
            } else if ($start > 0) {
                // 数据表中还有更多的数据 继续下一个1000条数据的备份
                continue;
            } else {
                // 未知错误
                $status = 0;
                break;
            }
        }
        // 解除文件锁
        unlink($config['path'] . 'backup.lock');
        if ($status == 0) {
            $this->error('备份出错');
        }
        $this->success('备份成功');
    }

    /**
     * 还原数据库
     * 修改：取消逐表响应方式，改用“堵塞式”还原。2019 01 14 icy8
     * @author 麦当苗儿 <zuojiazi@vip.qq.com>
     */
    public function import($name = 0)
    {
        //获取备份文件信息
        $name = $name . '-*.sql*';// 所有备份卷
        $path = realpath(C('DATA_BACKUP_PATH')) . DIRECTORY_SEPARATOR . $name;
        $files = glob($path);
        $list = array();
        foreach ($files as $name) {
            $basename = basename($name);
            $match = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
            $gz = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename);
            $list[$match[6]] = array($match[6], $name, $gz);
        }
        ksort($list);

        //检测文件正确性
        $last = end($list);
        $part = 1;
        $status = $start = 0;
        if (count($list) !== $last[0]) {
            $this->error('备份文件可能已经损坏，请检查！');
        }
        $k = 0;
        while (1) {
            $db = new Database($list[$part], array(
                'path' => realpath(C('DATA_BACKUP_PATH')) . DIRECTORY_SEPARATOR,
                'compress' => $list[$part][2]));
            $start = $db->import($start);
            if (0 === $start && !isset($list[++$part])) {
                //还原完成
                $status = 1;
                break;
            }elseif($start > 0){
                continue;
            } else {
                // 还原出错
                break;
            }
            $k++;
            if ($k > 10000) {
                break;
            }
        }
        if(!$status) {
            $this->error('还原出错');
        }
        $this->success('还原成功');
    }

}
